package org.aoju.bus.core.io.buffer;

import java.util.Objects;

/**
 * 循环缓冲区
 */
public class CircularBuffer {

    private final byte[] buffer;
    private int startOffset;
    private int endOffset;
    private int currentNumberOfBytes;

    /**
     * 默认缓冲大小的构造
     */
    public CircularBuffer() {
        this(2 << 12);
    }

    /**
     * 构造
     *
     * @param pSize 缓冲大小
     */
    public CircularBuffer(final int pSize) {
        buffer = new byte[pSize];
        startOffset = 0;
        endOffset = 0;
        currentNumberOfBytes = 0;
    }

    /**
     * 从buffer中读取下一个byte，同时移除这个bytes。
     *
     * @return The byte
     * @throws IllegalStateException buffer为空抛出，使用{@link #hasBytes()},或 {@link #getCurrentNumberOfBytes()}判断
     */
    public byte read() {
        if (currentNumberOfBytes <= 0) {
            throw new IllegalStateException("No bytes available.");
        }
        final byte b = buffer[startOffset];
        --currentNumberOfBytes;
        if (++startOffset == buffer.length) {
            startOffset = 0;
        }
        return b;
    }

    /**
     * Returns the given number of bytes from the buffer by storing them in
     * the given byte array at the given offset.
     * 从buffer中获取指定长度的bytes，从给定的targetBuffer的targetOffset位置写出
     *
     * @param targetBuffer 目标bytes
     * @param targetOffset 目标数组开始位置
     * @param length       读取长度
     * @throws NullPointerException     提供的数组为{@code null}
     * @throws IllegalArgumentException {@code targetOffset}或{@code length} 为负数或{@code targetBuffer}太小
     * @throws IllegalStateException    buffer中的byte不足，使用{@link #getCurrentNumberOfBytes()}判断。
     */
    public void read(final byte[] targetBuffer, final int targetOffset, final int length) {
        Objects.requireNonNull(targetBuffer);
        if (targetOffset < 0 || targetOffset >= targetBuffer.length) {
            throw new IllegalArgumentException("Invalid offset: " + targetOffset);
        }
        if (length < 0 || length > buffer.length) {
            throw new IllegalArgumentException("Invalid length: " + length);
        }
        if (targetOffset + length > targetBuffer.length) {
            throw new IllegalArgumentException("The supplied byte array contains only "
                    + targetBuffer.length + " bytes, but offset, and length would require "
                    + (targetOffset + length - 1));
        }
        if (currentNumberOfBytes < length) {
            throw new IllegalStateException("Currently, there are only " + currentNumberOfBytes
                    + "in the buffer, not " + length);
        }
        int offset = targetOffset;
        for (int i = 0; i < length; i++) {
            targetBuffer[offset++] = buffer[startOffset];
            --currentNumberOfBytes;
            if (++startOffset == buffer.length) {
                startOffset = 0;
            }
        }
    }

    /**
     * 增加byte到buffer中
     *
     * @param value The byte
     * @throws IllegalStateException buffer已满. 用{@link #hasSpace()}或{@link #getSpace()}判断。
     */
    public void add(final byte value) {
        if (currentNumberOfBytes >= buffer.length) {
            throw new IllegalStateException("No space available");
        }
        buffer[endOffset] = value;
        ++currentNumberOfBytes;
        if (++endOffset == buffer.length) {
            endOffset = 0;
        }
    }

    /**
     * Returns, whether the next bytes in the buffer are exactly those, given by
     * {@code sourceBuffer}, {@code offset}, and {@code length}. No bytes are being
     * removed from the buffer. If the result is true, then the following invocations
     * of {@link #read()} are guaranteed to return exactly those bytes.
     *
     * @param sourceBuffer the buffer to compare against
     * @param offset       start offset
     * @param length       length to compare
     * @return True, if the next invocations of {@link #read()} will return the
     * bytes at offsets {@code pOffset}+0, {@code pOffset}+1, ...,
     * {@code pOffset}+{@code pLength}-1 of byte array {@code pBuffer}.
     * @throws IllegalArgumentException Either of {@code pOffset}, or {@code pLength} is negative.
     * @throws NullPointerException     The byte array {@code pBuffer} is null.
     */
    public boolean peek(final byte[] sourceBuffer, final int offset, final int length) {
        Objects.requireNonNull(sourceBuffer, "Buffer");
        if (offset < 0 || offset >= sourceBuffer.length) {
            throw new IllegalArgumentException("Invalid offset: " + offset);
        }
        if (length < 0 || length > buffer.length) {
            throw new IllegalArgumentException("Invalid length: " + length);
        }
        if (length < currentNumberOfBytes) {
            return false;
        }
        int localOffset = startOffset;
        for (int i = 0; i < length; i++) {
            if (buffer[localOffset] != sourceBuffer[i + offset]) {
                return false;
            }
            if (++localOffset == buffer.length) {
                localOffset = 0;
            }
        }
        return true;
    }

    /**
     * Adds the given bytes to the buffer. This is the same as invoking {@link #add(byte)}
     * for the bytes at offsets {@code offset+0}, {@code offset+1}, ...,
     * {@code offset+length-1} of byte array {@code targetBuffer}.
     *
     * @param targetBuffer the buffer to copy
     * @param offset       start offset
     * @param length       length to copy
     * @throws IllegalStateException    The buffer doesn't have sufficient space. Use
     *                                  {@link #getSpace()} to prevent this exception.
     * @throws IllegalArgumentException Either of {@code pOffset}, or {@code pLength} is negative.
     * @throws NullPointerException     The byte array {@code pBuffer} is null.
     */
    public void add(final byte[] targetBuffer, final int offset, final int length) {
        Objects.requireNonNull(targetBuffer, "Buffer");
        if (offset < 0 || offset >= targetBuffer.length) {
            throw new IllegalArgumentException("Invalid offset: " + offset);
        }
        if (length < 0) {
            throw new IllegalArgumentException("Invalid length: " + length);
        }
        if (currentNumberOfBytes + length > buffer.length) {
            throw new IllegalStateException("No space available");
        }
        for (int i = 0; i < length; i++) {
            buffer[endOffset] = targetBuffer[offset + i];
            if (++endOffset == buffer.length) {
                endOffset = 0;
            }
        }
        currentNumberOfBytes += length;
    }

    /**
     * Returns, whether there is currently room for a single byte in the buffer.
     * Same as {@link #hasSpace(int) hasSpace(1)}.
     *
     * @return true if there is space for a byte
     * @see #hasSpace(int)
     * @see #getSpace()
     */
    public boolean hasSpace() {
        return currentNumberOfBytes < buffer.length;
    }

    /**
     * Returns, whether there is currently room for the given number of bytes in the buffer.
     *
     * @param count the byte count
     * @return true if there is space for the given number of bytes
     * @see #hasSpace()
     * @see #getSpace()
     */
    public boolean hasSpace(final int count) {
        return currentNumberOfBytes + count <= buffer.length;
    }

    /**
     * Returns, whether the buffer is currently holding, at least, a single byte.
     *
     * @return true if the buffer is not empty
     */
    public boolean hasBytes() {
        return currentNumberOfBytes > 0;
    }

    /**
     * Returns the number of bytes, that can currently be added to the buffer.
     *
     * @return the number of bytes that can be added
     */
    public int getSpace() {
        return buffer.length - currentNumberOfBytes;
    }

    /**
     * Returns the number of bytes, that are currently present in the buffer.
     *
     * @return the number of bytes
     */
    public int getCurrentNumberOfBytes() {
        return currentNumberOfBytes;
    }

    /**
     * Removes all bytes from the buffer.
     */
    public void clear() {
        startOffset = 0;
        endOffset = 0;
        currentNumberOfBytes = 0;
    }

}
